跳到主要内容

Go 逐行读取文件 Scanner

当文件过大,不适合一次性载入内存,且文件每行都包含特定信息时,我们就需要逐行读取文件来保证程序的性能;

Go 语言有三种逐行读取文件的方法,依次是:

  • ReadString
  • ReadLine
  • Scanner

下面分别介绍它们的用法

ReadString

ReadString 是一个位于 bufio 包的方法

func (b *Reader) ReadString(delim byte) (string, error)

ReadString 从输入中读取数据,直到分隔符出现,返回包括分割符在内的字符串。如果 ReadString 在找到一个分割符之前就遇到了错误,它会返回已读取的数据和错误本身。注意,这里需要裁切掉末尾的换行符

import (
"bufio"
"fmt"
"os"
"strings"
)

func main() {
fileObj, _ := os.Open("file.txt")
defer fileObj.Close()
reader := bufio.NewReader(fileObj)

for {
if str, ok := readline(reader); ok {
fmt.Println(str)
continue
}
break
}
}

func readline(fi *bufio.Reader) (string, bool) {
s, err := fi.ReadString('\n')
if err != nil {
return "", false
}
return strings.Trim(s, "\r\n"), true
}

更多示例参考 Golang Reader.ReadString Examples

2、ReadLine 不介绍,这个是 bufio 实现 ReadString 的方法,但是注释上已经写了不建议使用

Scanner

下面就是这次的主角 Scanner 工具,它也是 bufio 包下的一个工具,从字面意思来看是一个扫描器、扫描仪。

所用是不停的从一个 reader 中读取数据兵缓存在内存中,还提供了一个注入函数用来自定义分割符。库中还提供了4个预定义分割方法。

  • ScanLines:以换行符分割('n')
  • ScanWords:返回通过“空格”分词的单词
  • ScanRunes:返回单个 UTF-8 编码的 rune 作为一个 token
  • ScanBytes:返回单个字节作为一个 token

使用例:

package main

import (
"fmt"
"os"
"bufio"
)

func main() {
fp,err := os.Open("file.txt")
if err!=nil{
fmt.Println(err) //打开文件错误
return
}

buf := bufio.NewScanner(fp)
for {
if !buf.Scan() {
break //文件读完了,退出for
}
line := buf.Text() //获取每一行
fmt.Println(line)
}
}

替换分割方式

上面说到 Scanner 提供了四个默认的分割方法,如何使用呢?只需简单的传入就行了(它是一个回调函数)

func main() {
fp, err := os.Open("./LICENSE")
if err != nil {
fmt.Println(err) //打开文件错误
return
}
scanner := bufio.NewScanner(fp)
scanner.Split(bufio.ScanWords) // 替换分割方式
for {

if !scanner.Scan() {
break //文件读完了,退出for
}
line := scanner.Text() //获取每一行
fmt.Println(line)
}
}

输出:

Public
License.
Notwithstanding
any
other
provision
of
this
License,
...

从指定行号开始读取

bufio.Scanner 默认不保存行号,所以这里需要简单拓展一下,这里使用 Seeker 接口来用做读取的偏移量

这里不使用默认的切割函数自定义一个 bufio.SplitFunc,这个函数签名为:

type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)

它返回的 advance 表示前进了多少位,token 表示本次切割从起点到终点所包含的内容,所以这里直接利用原本的 ScanLines 进行拓展。

func withScanner(input io.ReadSeeker, start int64) error {
fmt.Println("--SCANNER, start:", start)
if _, err := input.Seek(start, 0); err != nil {
return err
}
scanner := bufio.NewScanner(input)

pos := start
scanLines := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
advance, token, err = bufio.ScanLines(data, atEOF)
pos += int64(advance)
return
}
scanner.Split(scanLines)

for scanner.Scan() {
fmt.Printf("Pos: %d, Scanned: %s\n", pos, scanner.Text())
}
return scanner.Err()
}

References

How to read a file starting from a specific line number using Scanner? How to use bufio.ScanWords Scanner Documentation Golang 逐行读写之scanner.Scan